/**
 *  Copyright (C) 2008-2009  The Aceituna Project Team
 *  Portions of this code are Copyright(C) 2002-2007  The FreeCol Team (www.freecol.org).
 *
 *  This file is part of Aceituna Project.
 *
 *  Aceituna Project is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  Aceituna Project is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with Aceituna Project.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.aceitunaproject.mapdisplay;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.util.logging.Logger;
import javax.swing.JComponent;


import com.aceitunaproject.mapdisplay.config.DisplayConfigurationProvider;
import com.aceitunaproject.mapdisplay.model.Thing;
import com.aceitunaproject.mapdisplay.model.map.Map;
import com.aceitunaproject.mapdisplay.model.map.Direction;
import com.aceitunaproject.mapdisplay.model.map.Position;
import com.aceitunaproject.mapdisplay.model.map.Map.WholeMapIterator;
import com.aceitunaproject.mapdisplay.model.map.tile.Tile;
import com.aceitunaproject.mapdisplay.resources.ImageLibrary;

/**
 * This class is responsible for drawing the map. In addition, the graphical
 * state of the map (focus,etc) is also a responsibility of this class.
 * 
 * @author Originally from FreeCol (http://sourceforge.net/projects/freecol),
 *         modified for the Aceituna Project by Federico Bergstein
 * 
 */
public final class MapDrawerImpl implements MapDrawer {

	private static final Logger logger = Logger.getLogger(MapDrawerImpl.class
			.getName());

	// the size of the window
	private Dimension size;

	// the map object
	private Map map;

	// the focus
	private Position focus = null;

	// Helper variables for displaying the map.
	private int tileHeight = DisplayConfigurationProvider.getConfig()
			.getTileHeight();
	private int tileWidth = DisplayConfigurationProvider.getConfig()
			.getTileWidth();;
	private int topSpace;
	private int topRows;
	private int bottomRows;
	private int leftSpace;
	private int rightSpace;

	// The y-coordinate of the Tiles that will be drawn at the bottom
	private int bottomRow = -1;
	// The y-coordinate of the Tiles that will be drawn at the top
	private int topRow;
	// The y-coordinate on the screen (in pixels) of the images of the
	// Tiles that will be drawn at the bottom
	private int bottomRowY;
	// The y-coordinate on the screen (in pixels) of the images of the
	// Tiles that will be drawn at the top
	private int topRowY;

	// The x-coordinate of the Tiles that will be drawn at the left side
	private int leftColumn;
	// The x-coordinate of the Tiles that will be drawn at the right side
	private int rightColumn;
	// The x-coordinate on the screen (in pixels) of the images of the
	// Tiles that will be drawn at the left (can be less than 0)
	private int leftColumnX;

	private boolean displayGrid = false;

	// the pane where the map will be drawn
	private JComponent pane;

	private Position highlighted;

	/**
	 * 
	 * Displays a tile in the map on a given Graphics2D.
	 * 
	 * @param g
	 *            the Graphics2D on wich the map will be drawn.
	 * @param map
	 *            the Map object that contains the tile.
	 * @param tile
	 *            the tile to draw.
	 * @param x
	 *            the X coordinate of the tile.
	 * @param y
	 *            the Y coordinate.
	 * @param drawUnexploredBorders
	 *            wether or not unexplored borders will be drawn.
	 */
	private void displayATile(Graphics2D g, Map map, Tile tile, int x, int y,
			boolean drawUnexploredBorders) {
		if (tile == null) {
			return;
		}

		if (displayGrid) {

			g.drawLine(x - tileWidth / 2, y, x, y + tileHeight / 2);
			g.drawLine(x - tileWidth / 2, y - tileHeight, x + tileWidth / 2, y
					- tileHeight * 2);

		}

		// draws the tile
		g.drawImage(ImageLibrary.getBasicTileImage(tile.getBasicTileType(), x,
				y), x, y, null);

		Position pos = new Position(tile.getX(), tile.getY());
		// logger.info("drawing tile " + pos.toString());
		// g.drawString("L", x - tileWidth / 2, y);
		// g.drawString("R", x + tileWidth / 2, y);
		// g.drawString("T", x, y - tileHeight / 2);
		// g.drawString("B", x, y + tileHeight / 2);
		tile.setLeft(x);
		tile.setRight(x + tileWidth);
		tile.setBottom(y + 96);
		tile.setTop(y);

		tile.area = new Polygon(new int[] { x, x + tileWidth / 2,
				x + tileWidth, x + tileWidth / 2 }, new int[] {
				y + tileHeight / 2, y + tileHeight, y + tileHeight / 2, y }, 4);

		g.draw(tile.area);

		// g.fill(tile.area);

		// if (tile.getX() == 1 && tile.getY() == 1)
		// g.fill(tile.area);

		g.drawString(pos.toString(), x + tileWidth / 2, y + tileHeight / 2);
		// draws the borders

		for (Direction direction : Direction.values()) {
			Position p = Map.getAdjacent(pos, direction);
			if (map.isValid(p)) {
				Tile borderingTile = map.getTile(p.x, p.y);
				if (borderingTile == null) {
					// this really shouldn't happen
					logger.warning("Bordering Tile was null.");
					continue;
				}

				if (tile.getBasicTileType() == borderingTile.getBasicTileType()) {
					// * Equal tiles, no need to draw border continue;
					// TODO: its using the length of the tile type name to
					// determine which tile border prevails, this should be
					// changed.
				} else if (tile.getBasicTileType().length() <= borderingTile
						.getBasicTileType().length()// borderingTile.
				) {
					g.drawImage(ImageLibrary.getBorderTileImage(borderingTile
							.getBasicTileType(), direction, x, y), x, y, null);

				}
			}
		}

		// draws the things in the tile
		for (Thing thing : tile.getThingList()) {
			displayThing(g, thing, x, y);

		}

	}

	/**
	 * The constructor to use.
	 * 
	 * @param pane
	 *            The desktop pane where the map will be drawn.
	 * 
	 * @param map
	 *            The map object to draw.
	 * 
	 * @param size
	 *            The size of the pane.
	 **/

	public MapDrawerImpl(JComponent pane, Map map, Dimension size) {

		this.map = map;
		this.size = size;
		this.pane = pane;

		logger.info("Map display created/refreshed.");

		updateMapDisplayVariables();

	}

	/**
	 * Updates the Map display variables
	 */
	private void updateMapDisplayVariables() {
		// Calculate the amount of rows that will be drawn above the central
		// Tile
		topSpace = (size.height - tileHeight) / 2;
		if ((topSpace % (tileHeight / 2)) != 0) {
			topRows = topSpace / (tileHeight / 2) + 2;
		} else {
			topRows = topSpace / (tileHeight / 2) + 1;
		}
		bottomRows = topRows;
		leftSpace = (size.width - tileWidth) / 2;
		rightSpace = leftSpace;
	}

	public void setSize(Dimension size) {
		this.size = size;
		updateMapDisplayVariables();
	}

	public Thing getThingInFront(Tile thingTile) {
		if (thingTile == null || thingTile.getThingCount() <= 0) {
			return null;
		}
		return thingTile.getThingList().get(0);

	}

	/**
	 * 
	 * sets the focus to a given Position
	 * 
	 * @param focus
	 *            the Position within the map that will get the Focus.
	 */
	public void setFocus(Position focus) {
		this.focus = focus;

		forceReposition();
		getPane().repaint(0, 0, getWidth(), getHeight());
	}

	/**
	 * 
	 * sets the focus to a given Position immediately
	 * 
	 * @param focus
	 *            the Position within the map that will get the Focus.
	 */
	public void setFocusImmediately(Position focus) {
		this.focus = focus;

		forceReposition();
		getPane().paintImmediately(0, 0, getWidth(), getHeight());
	}

	/**
	 * returns the pane where the map must be drawn
	 * 
	 * @return the pane in which the map should be drawn
	 */
	private JComponent getPane() {
		return pane;
	}

	public void setFocus(int x, int y) {
		setFocus(new Position(x, y));
	}

	public int getWidth() {
		return size.width;
	}

	public int getHeight() {
		return size.height;
	}

	public void display(Graphics2D g) {

		displayMap(g);
		if (highlighted != null)
			logger.info("highlighted = " + highlighted.toString());

	}

	/**
	 * Returns the amount of columns that are to the left of the Tile that is
	 * displayed in the center of the Map.
	 * 
	 * @return The amount of columns that are to the left of the Tile that is
	 *         displayed in the center of the Map.
	 */
	private int getLeftColumns() {
		return getLeftColumns(focus.getY());
	}

	/**
	 * Returns the amount of columns that are to the left of the Tile with the
	 * given y-coordinate.
	 * 
	 * @param y
	 *            The y-coordinate of the Tile in question.
	 * @return The amount of columns that are to the left of the Tile with the
	 *         given y-coordinate.
	 */
	private int getLeftColumns(int y) {
		int leftColumns;

		if ((y % 2) == 0) {
			leftColumns = leftSpace / tileWidth + 1;
			if ((leftSpace % tileWidth) > 32) {
				leftColumns++;
			}
		} else {
			leftColumns = leftSpace / tileWidth + 1;
			if ((leftSpace % tileWidth) == 0) {
				leftColumns--;
			}
		}

		return leftColumns;
	}

	/**
	 * Returns the amount of columns that are to the right of the Tile that is
	 * displayed in the center of the Map.
	 * 
	 * @return The amount of columns that are to the right of the Tile that is
	 *         displayed in the center of the Map.
	 */
	private int getRightColumns() {
		return getRightColumns(focus.getY());
	}

	/**
	 * Returns the amount of columns that are to the right of the Tile with the
	 * given y-coordinate.
	 * 
	 * @param y
	 *            The y-coordinate of the Tile in question.
	 * @return The amount of columns that are to the right of the Tile with the
	 *         given y-coordinate.
	 */
	private int getRightColumns(int y) {
		int rightColumns;

		if ((y % 2) == 0) {
			rightColumns = rightSpace / tileWidth + 1;
			if ((rightSpace % tileWidth) == 0) {
				rightColumns--;
			}
		} else {
			rightColumns = rightSpace / tileWidth + 1;
			if ((rightSpace % tileWidth) > 32) {
				rightColumns++;
			}
		}

		return rightColumns;
	}

	/**
	 * Position the map so that the Tile at the focus location is displayed at
	 * the center.
	 */
	private void positionMap() {

		if (focus == null) {
			return;
		}

		int x = focus.getX(), y = focus.getY();
		int leftColumns = getLeftColumns(), rightColumns = getRightColumns();

		/*
		 * PART 1 ====== Calculate: bottomRow, topRow, bottomRowY, topRowY This
		 * will tell us which rows need to be drawn on the screen (from
		 * bottomRow until and including topRow). bottomRowY will tell us at
		 * which height the bottom row needs to be drawn.
		 */

		if (y < topRows) {
			// We are at the top of the map
			bottomRow = (size.height / (tileHeight / 2)) - 1;
			if ((size.height % (tileHeight / 2)) != 0) {
				bottomRow++;
			}
			topRow = 0;
			bottomRowY = bottomRow * (tileHeight / 2);
			topRowY = 0;
		} else if (y >= (getMap().getHeight() - bottomRows)) {
			// We are at the bottom of the map
			bottomRow = getMap().getHeight() - 1;

			topRow = size.height / (tileHeight / 2);
			if ((size.height % (tileHeight / 2)) > 0) {
				topRow++;
			}
			topRow = getMap().getHeight() - topRow;

			bottomRowY = size.height - tileHeight;
			topRowY = bottomRowY - (bottomRow - topRow) * (tileHeight / 2);
		} else {
			// We are not at the top of the map and not at the bottom
			bottomRow = y + bottomRows;
			topRow = y - topRows;
			bottomRowY = topSpace + (tileHeight / 2) * bottomRows;
			topRowY = topSpace - topRows * (tileHeight / 2);
		}

		/*
		 * PART 2 ====== Calculate: leftColumn, rightColumn, leftColumnX This
		 * will tell us which columns need to be drawn on the screen (from
		 * leftColumn until and including rightColumn). leftColumnX will tell us
		 * at which x-coordinate the left column needs to be drawn (this is for
		 * the Tiles where y%2==0; the others should be tileWidth / 2 more to
		 * the right).
		 */

		if (x < leftColumns) {
			// We are at the left side of the map
			leftColumn = 0;

			rightColumn = size.width / tileWidth - 1;
			if ((size.width % tileWidth) > 0) {
				rightColumn++;
			}

			leftColumnX = 0;
		} else if (x >= (getMap().getWidth() - rightColumns)) {
			// We are at the right side of the map
			rightColumn = getMap().getWidth() - 1;

			leftColumn = size.width / tileWidth;
			if ((size.width % tileWidth) > 0) {
				leftColumn++;
			}

			leftColumnX = size.width - tileWidth - tileWidth / 2 - leftColumn
					* tileWidth;
			leftColumn = rightColumn - leftColumn;
		} else {
			// We are not at the left side of the map and not at the right side
			leftColumn = x - leftColumns;
			rightColumn = x + rightColumns;
			leftColumnX = (size.width - tileWidth) / 2 - leftColumns
					* tileWidth;
		}
	}

	private int getXOffset(int clipLeftX, int tileY) {
		int xx = clipLeftX;
		if ((tileY % 2) != 0) {
			xx += tileWidth / 2;
		}
		return xx;
	}

	/**
	 * Displays the Map onto the given Graphics2D object.
	 * 
	 * @param g
	 *            The Graphics2D object on which to draw the Map.
	 */
	void displayMap(Graphics2D g) {
		Rectangle clipBounds = g.getClipBounds();
		g.fillRect(clipBounds.x, clipBounds.y, clipBounds.width,
				clipBounds.height);
		Map map = getMap();
		int xx;
		int yy = 0;

		// Row per row; start with the top modified row
		for (int tileY = 0; tileY <= map.getHeight(); tileY++) {
			xx = getXOffset(0, tileY);

			// Column per column; start at the left side to display the tiles.
			for (int tileX = 0; tileX <= map.getWidth(); tileX++) {
				displayATile(g, map, map.getTile(tileX, tileY), xx, yy, true);
				xx += tileWidth;
			}

			yy += tileHeight / 2;
		}
		// if (tile.getX() == 2 && tile.getY() == 2)
		if (highlighted != null) {
			g.setColor(Color.YELLOW);
			g.draw(map.getTile(highlighted).area);
			g.setColor(Color.BLACK);

		}

	}

	/**
	 * Displays the Map onto the given Graphics2D object.
	 * 
	 * @param g
	 *            The Graphics2D object on which to draw the Map.
	 */
	void displayMapOri(Graphics2D g) {
		Rectangle clipBounds = g.getClipBounds();
		Map map = getMap();

		/*
		 * PART 1 ====== Position the map if it is not positioned yet.
		 */

		if (bottomRow < 0) {
			positionMap();
		}

		/*
		 * PART 1a ======= Determine which tiles need to be redrawn.
		 */
		int clipTopRow = (clipBounds.y - topRowY) / (tileHeight / 2) - 1;
		int clipTopY = topRowY + clipTopRow * (tileHeight / 2);
		clipTopRow = topRow + clipTopRow;

		int clipLeftCol = (clipBounds.x - leftColumnX) / tileWidth - 1;
		int clipLeftX = leftColumnX + clipLeftCol * tileWidth;
		clipLeftCol = leftColumn + clipLeftCol;

		int clipBottomRow = (clipBounds.y + clipBounds.height - topRowY)
				/ (tileHeight / 2);
		clipBottomRow = topRow + clipBottomRow;

		int clipRightCol = (clipBounds.x + clipBounds.width - leftColumnX)
				/ tileWidth;
		clipRightCol = leftColumn + clipRightCol;

		/*
		 * PART 2 ====== Display the Tiles and the Units.
		 */

		g.setColor(Color.black);
		g.fillRect(clipBounds.x, clipBounds.y, clipBounds.width,
				clipBounds.height);
		int xx;

		/*
		 * PART 2a ======= Display the base Tiles
		 */

		int yy = clipTopY;

		// Row per row; start with the top modified row
		for (int tileY = clipTopRow; tileY <= clipBottomRow; tileY++) {
			xx = getXOffset(clipLeftX, tileY);

			// Column per column; start at the left side to display the tiles.
			for (int tileX = clipLeftCol; tileX <= clipRightCol; tileX++) {
				displayATile(g, map, map.getTile(tileX, tileY), xx, yy, true);
				xx += tileWidth;
			}

			yy += tileHeight / 2;
		}

	}

	public void setDisplayGrid(boolean displayGrid) {
		this.displayGrid = displayGrid;
	}

	/**
	 * Displays the given Thing onto the given Graphics2D object at the location
	 * specified by the coordinates.
	 * 
	 * @param g
	 *            The Graphics2D object on which to draw the Thing.
	 * @param thing
	 *            The Thing to draw.
	 * @param x
	 *            The x-coordinate of the location where to draw the Thing (in
	 *            pixels). These are the coordinates of the Tile on which the
	 *            Thing is located.
	 * @param y
	 *            The y-coordinate of the location where to draw the Thing (in
	 *            pixels). These are the coordinates of the Tile on which the
	 *            Thing is located.
	 */
	private void displayThing(Graphics2D g, Thing thing, int x, int y) {

		Image image = ImageLibrary.getImage(thing.getType());
		Point p = getThingImagePositionInTile(image, x, y);
		g.drawImage(image, p.x, p.y, null);

	}

	/**
	 * Gets the coordinates to draw a thing in a given tile.
	 * 
	 * @param thingImage
	 *            The thing's image
	 * @param tileX
	 *            The X coordinate of the tile
	 * @param tileY
	 *            The Y coordinate of the tile
	 * @return The coordinates where the thing should be drawn onscreen
	 */
	private Point getThingImagePositionInTile(Image thingImage, int tileX,
			int tileY) {
		return getThingImagePositionInTile(thingImage.getWidth(null),
				thingImage.getHeight(null), tileX, tileY);
	}

	/**
	 * Gets the coordinates to draw a thing in a given tile.
	 * 
	 * @param thingImageWidth
	 *            The thing image's width
	 * @param thingImageHeight
	 *            The thing image's height
	 * @param tileX
	 *            The X coordinate of the tile
	 * @param tileY
	 *            The Y coordinate of the tile
	 * @return The coordinates where the thing should be drawn onscreen
	 */
	private Point getThingImagePositionInTile(int thingImageWidth,
			int thingImageHeight, int tileX, int tileY) {
		int thingX = ((tileX + getTileWidth() / 2) - thingImageWidth / 2);
		int thingY = (tileY + getTileHeight() / 2) - thingImageHeight / 2
				- (int) (getThingOffSet() * libgetScalingFactor());

		return new Point(thingX, thingY);
	}

	/**
	 * 
	 * @return The height offset to paint a Thing at (in pixels).
	 */
	private int getThingOffSet() {
		return DisplayConfigurationProvider.getConfig().getThingOffSet();
	}

	/**
	 * 
	 * @return the scaling factor to use in MapDrawer implementations
	 */
	private int libgetScalingFactor() {

		return DisplayConfigurationProvider.getConfig().getScalingFactor();

	}

	public boolean onScreen(int x, int y) {
		if (bottomRow < 0) {
			positionMap();
			return y - 2 > topRow && y + 4 < bottomRow && x - 1 > leftColumn
					&& x + 2 < rightColumn;
		} else {
			return y - 2 > topRow && y + 4 < bottomRow && x - 1 > leftColumn
					&& x + 2 < rightColumn;
		}
	}

	public boolean onScreen(Position position) {
		return onScreen(position.getX(), position.getY());
	}

	public Position convertToMapCoordinates(int x, int y) {


		int yy = -1;
	
		int xx = -1;

		
		WholeMapIterator mi = map.getWholeMapIterator();

		while (mi.hasNext()) {
			Position p = mi.next();
			Tile t = map.getTile(p);
			// if(t.area.contains(x,y))
			if (t.area.contains(new Point(x, y))) {
				xx = p.x;
				yy = p.y;
				return new Position(xx, yy);
			}

			
		}

		return new Position(xx, yy);

	}

	
	public Map getMap() {

		return map;
	}

	public boolean isMapNearTop(int y) {
		if (y < topRows) {
			return true;
		} else {
			return false;
		}
	}

	public boolean isMapNearBottom(int y) {
		if (y >= (getMap().getHeight() - bottomRows)) {
			return true;
		} else {
			return false;
		}
	}

	public boolean isMapNearLeft(int x, int y) {
		if (x < getLeftColumns(y)) {
			return true;
		} else {
			return false;
		}
	}

	public boolean isMapNearRight(int x, int y) {
		if (x >= (getMap().getWidth() - getRightColumns(y))) {
			return true;
		} else {
			return false;
		}
	}

	public Point getTilePosition(Tile t) {
		if (t.getY() >= topRow && t.getY() <= bottomRow
				&& t.getX() >= leftColumn && t.getX() <= rightColumn) {
			int x = ((t.getX() - leftColumn) * tileWidth) + leftColumnX;
			int y = ((t.getY() - topRow) * tileHeight / 2) + topRowY;
			if ((t.getY() % 2) != 0) {
				x += tileWidth / 2;
			}
			return new Point(x, y);
		} else {
			return null;
		}
	}

	public Rectangle getTileBounds(Tile tile) {
		return getTileBounds(tile.getX(), tile.getY());
	}

	public Rectangle getTileBounds(int x, int y) {
		Rectangle result = new Rectangle(0, 0, size.width, size.height);
		if (y >= topRow && y <= bottomRow && x >= leftColumn
				&& x <= rightColumn) {
			result.y = ((y - topRow) * tileHeight / 2) + topRowY - tileHeight;
			result.x = ((x - leftColumn) * tileWidth) + leftColumnX;
			if ((y % 2) != 0) {
				result.x += tileWidth / 2;
			}
			result.width = tileWidth;
			result.height = tileHeight * 2;
		}
		return result;
	}

	public void forceReposition() {
		bottomRow = -1;
	}

	public int getTileHeight() {
		return tileHeight;
	}

	public int getTileWidth() {
		return tileWidth;
	}

	public void highlightTile(int x, int y) {

		highlighted = new Position(x, y);

	}
}
